package de.justphil.tcg.tcgserver.rest.resources;

import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.locks.Lock;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;

import org.slf4j.Logger;

import static de.justphil.tcg.tcgserver.commons.logging.TCGLogging.*;
import de.justphil.tcg.tcgserver.commons.domain.Game;
import de.justphil.tcg.tcgserver.commons.domain.User;
import de.justphil.tcg.tcgserver.commons.domain.Game.State;
import de.justphil.tcg.tcgserver.commons.domain.containers.GameExpired;
import de.justphil.tcg.tcgserver.commons.domain.containers.GameOver;
import de.justphil.tcg.tcgserver.commons.domain.containers.NewGame;
import de.justphil.tcg.tcgserver.commons.domain.wrappers.NewGamePlayerOneRes;
import de.justphil.tcg.tcgserver.commons.domain.wrappers.NewGameRes;
import de.justphil.tcg.tcgserver.commons.domain.wrappers.SingleObjectRes;
import de.justphil.tcg.tcgserver.commons.exceptions.TCGException;
import de.justphil.tcg.tcgserver.commons.grid.Grid;
import de.justphil.tcg.tcgserver.commons.i18n.Error;
import de.justphil.tcg.tcgserver.commons.util.OTPGenerator;
import de.justphil.tcg.tcgserver.commons.util.OTPHelper;
import de.justphil.tcg.tcgserver.commons.config.Config;
import de.justphil.tcg.tcgserver.commons.config.RestConfig;
import de.justphil.tcg.tcgserver.commons.management.DataManager;
import de.justphil.tcg.tcgserver.commons.management.Application;
import de.justphil.tcg.tcgserver.rest.resources.base.AbstractResource;
import de.justphil.tcg.tcgserver.rest.resources.helpers.GameHelper;
import de.justphil.tcg.tcgserver.rest.services.GameService;
import de.justphil.tcg.tcgserver.rest.validators.NewGameValidator;

@Path("/game")
public class GameResource extends AbstractResource {
	private static final Logger log = restLogger(GameResource.class);
	
	private DataManager dataManager;
	
	public GameResource() {
		dataManager = Application.getDataManager();
	}
	
	
	
	@POST
	@Path("/new")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response createNewGame( NewGame newGame,
    							@HeaderParam(RestConfig.REQ_HEADER_ACCEPT_LANGUAGE) String locale,
    							@HeaderParam(RestConfig.REQ_HEADER_AUTHORIZATION) String authHeader) {
		
		User user = aaa(dataManager, locale, authHeader);
		
		try {
			// Validate input data
			new NewGameValidator().validatePost(dataManager, user, newGame);
			
			// Create new 'Game' object
			Game game = new Game(State.WAITING);
			game.getPlayers().add( user.getId() );
			
			// Store the object in cluster + db (write delay)
			dataManager.insertGame(game.getId(), game);
			
			// Initialize 'NewGame' object's internal data structures
			newGame.init();
			
			// Fill the object with data
			newGame.setGameId(game.getId());
			newGame.getPlayerIds().add(user.getId());
			newGame.getDecks()[0].setCollection(dataManager.getDeckById(newGame.getPlayerDeckId()).getCardInstances());
			newGame.getPlayerToDeckMapping().put(user.getId(), 0);
			
			// Generate and store player OTPs
			Random r = new Random(System.currentTimeMillis());
			
			String[] otps = newGame.getOtps();
			for (int i = 0; i < otps.length; i++) {
				otps[i] = OTPGenerator.otp(r.nextLong());
			}
			
			newGame.getPlayerToOtpMapping().put(user.getId(), 0);
			
			// Generate game OTP (aka game code)
			String otp = OTPHelper.generateOTP(dataManager, game.getId());
			
			// Store game OTP
			newGame.setGameOtp(otp);
			
			// Store 'NewGame' object in cluster + db (write delay)
			dataManager.insertNewGame(otp, newGame);
			
			log.info("New game created: gameId => {}, otp => '{}'", game.getId(), otp);
			
			return created( new SingleObjectRes<NewGamePlayerOneRes>(
					new NewGamePlayerOneRes(otp, RestConfig.NEW_GAME_POLL_INTERVAL)
			));
		}
		catch (TCGException e) {
			return err(e.getMessage(), (Status) e.getExtra(), locale);
		}
    }
	
	@PUT
	@Path("/new/{otp}")
	@Produces(MediaType.APPLICATION_JSON)
	@Consumes(MediaType.APPLICATION_JSON)
	public Response updateNewGame(	NewGame playerTwo,
									@PathParam("otp") String otp,
									@HeaderParam(RestConfig.REQ_HEADER_ACCEPT_LANGUAGE) String locale,
									@HeaderParam(RestConfig.REQ_HEADER_AUTHORIZATION) String authHeader) {
		
		User user = aaa(dataManager, locale, authHeader);
		
		/*
		 * Critical Area, which must be synchronized, consists of:
		 * 
		 * - updateNewGame()
		 * - deleteNewGame()
		 * - onGameExpired()
		 * - onGameOver()
		 * 
		 */
		
		Lock lock = null;
		long gameId = 0;
		
		try {
			NewGame newGame = new NewGameValidator().validatePut(dataManager, user, otp, playerTwo);
			
			// Lock this game
			lock = Grid.getGameLock( newGame.getGameId() );
			lock.lock();
			gameId = newGame.getGameId();
			log.debug("updateNewGame: Locked game {}!", gameId);
			
			/* 
			 * Again try to get the NewGame object out of the cluster, because while waiting on the lock,
			 * it may happen that another thread has meanwhile removed this object.
			 * 
			 */
			newGame = dataManager.getNewGameByOTP(otp); // throws TCGException, if this NewGame object has meanwhile been removed
			
			// Fill 'NewGame' object with additional info related to player two
			newGame.getPlayerIds().add(user.getId());
			newGame.getDecks()[1].setCollection(dataManager.getDeckById(playerTwo.getPlayerDeckId()).getCardInstances());
			newGame.getPlayerToDeckMapping().put(user.getId(), 1);
			newGame.getPlayerToOtpMapping().put(user.getId(), 1);
			
			// Populate in-game cards for both players
			GameHelper.populateNewGameWithInGameCards(dataManager, newGame);
			
			// Update object in the cluster
			dataManager.updateNewGame(otp, newGame);
			
			// Use the 'NewGame' object in order to create a new game on the WebSocket server.
			GameService gameService = GameService.getInstance();
			boolean webSocketGameCreated = gameService.sendObject(newGame);
			
			if (!webSocketGameCreated) {
				cleanUp(otp, newGame.getGameId());
				throw new TCGException(Error.SERVER_ERROR_INTERNAL, Response.Status.INTERNAL_SERVER_ERROR);
			}
			
			Game game = dataManager.getGameById(newGame.getGameId());
			
			// Update 'Game' object with new state
			game.setState(State.PENDING);
			game.getPlayers().add( user.getId() );
			dataManager.updateGame(game.getId(), game);
			
			// TODO: Kill debug code.
			/* ######################################################################## */
			/* ######################################################################## */
			/* ######################################################################## */
			log.debug("newGame.getPlayerIds() => {}", newGame.getPlayerIds().toString());
			log.debug("newGame.getDecks()");
			for(int i = 0; i < newGame.getDecks().length; i++) {
				log.debug("newGame.getDecks()[{}] => {}", i, newGame.getDecks()[i].toString());
			}
			log.debug("newGame.getOtps() => {}", Arrays.toString(newGame.getOtps()));
			log.debug("newGame.getPlayerToDeckMapping()");
			for(Long lo : newGame.getPlayerToDeckMapping().keySet()) {
				log.debug("{} => {}", lo, newGame.getPlayerToDeckMapping().get(lo));
			}
			log.debug("newGame.getPlayerToOtpMapping()");
			for(Long lo : newGame.getPlayerToOtpMapping().keySet()) {
				log.debug("{} => {}", lo, newGame.getPlayerToOtpMapping().get(lo));
			}
			/* ######################################################################## */
			/* ######################################################################## */
			/* ######################################################################## */
			
			return ok( new SingleObjectRes<NewGameRes>(
					new NewGameRes(newGame.getGameId(), Config.WEBSOCKET_SERVER_URI, newGame.getOtps()[1])
			));
		}
		catch (TCGException e) {
			return err(e.getMessage(), (Status) e.getExtra(), locale);
		}
		finally {
			if (lock != null) {
				lock.unlock();
				log.debug("updateNewGame: Unlocked game {}!", gameId);
			}
		}
	}
	
	@DELETE
	@Path("/new/{otp}")
	@Produces(MediaType.APPLICATION_JSON)
	public Response deleteNewGame(	@PathParam("otp") String otp,
    								@HeaderParam(RestConfig.REQ_HEADER_ACCEPT_LANGUAGE) String locale,
    								@HeaderParam(RestConfig.REQ_HEADER_AUTHORIZATION) String authHeader) {
    	
		User user = aaa(dataManager, locale, authHeader);
		
		/*
		 * Critical Area, which must be synchronized, consists of:
		 * 
		 * - updateNewGame()
		 * - deleteNewGame()
		 * - onGameExpired()
		 * - onGameOver()
		 * 
		 */
		
		Lock lock = null;
		long gameId = 0;
		
		try {
			NewGame newGame = new NewGameValidator().validateDelete(dataManager, user, otp);
			
			// Lock this game
			lock = Grid.getGameLock( newGame.getGameId() );
			lock.lock();
			gameId = newGame.getGameId();
			log.debug("deleteNewGame: Locked game {}!", gameId);
			
			cleanUp(otp, newGame.getGameId());
			
			log.info("New game deleted: gameId => {}, otp => '{}'", newGame.getGameId(), otp);
			
			return ok();
		}
		catch (TCGException e) {
			return err(e.getMessage(), (Status) e.getExtra(), locale);
		}
		finally {
			if (lock != null) {
				lock.unlock();
				log.debug("deleteNewGame: Unlocked game {}!", gameId);
			}
		}
    }
	
	@GET
	@Path("/new/{otp}")
	@Produces(MediaType.APPLICATION_JSON)
	public Response getNewGame(	@PathParam("otp") String otp,
    							@HeaderParam(RestConfig.REQ_HEADER_ACCEPT_LANGUAGE) String locale,
    							@HeaderParam(RestConfig.REQ_HEADER_AUTHORIZATION) String authHeader) {
    	
		User user = aaa(dataManager, locale, authHeader);
		
		try {
			NewGame newGame = new NewGameValidator().validateGet(dataManager, user, otp);
			
			Game game = dataManager.getGameById(newGame.getGameId());
			
			if (game.getState() == State.WAITING) {
				log.info(	"Game creator is polling: gameId => {}, otp => '{}', result => '{}'",
							new Object[] { newGame.getGameId(), otp, "not modified" }
						);
				
				return notModified();
			}
			else {
				log.info(	"Game creator is polling: gameId => {}, otp => '{}', result => '{}'",
							new Object[] { newGame.getGameId(), otp, "ok" }
					);
				
				return ok( new SingleObjectRes<NewGameRes>(
						new NewGameRes(newGame.getGameId(), Config.WEBSOCKET_SERVER_URI, newGame.getOtps()[0])
				));
			}
		}
		catch (TCGException e) {
			return err(e.getMessage(), (Status) e.getExtra(), locale);
		}
    }
	
	/* ######################################################################## */
	/* ######################################################################## */
	/* ######################################################################## */
	
	/* Public methods that are NOT invoked by users, but by the GameService */
	
	public void onGameExpired(GameExpired gameExpired) {
		log.debug("onGameExpired() # gameId => {}", gameExpired.getGameId());
		
		/*
		 * Critical Area, which must be synchronized, consists of:
		 * 
		 * - updateNewGame()
		 * - deleteNewGame()
		 * - onGameExpired()
		 * - onGameOver()
		 * 
		 */
		
		Lock lock = null;
		long gameId = 0;
		
		try {
			// Lock this game
			lock = Grid.getGameLock( gameExpired.getGameId() );
			lock.lock();
			gameId = gameExpired.getGameId();
			log.debug("onGameExpired: Locked game {}!", gameId);
			
			Game game = dataManager.getGameById(gameExpired.getGameId());
			game.setState(State.GAME_EXPIRED);
			dataManager.updateGame(game.getId(), game);
			
			// Delete corresponding NewGame object in cluster
			cleanUpNewGame(gameExpired.getGameOtp());
		}
		catch (TCGException ignored) {
			log.error("TCGException during onGameExpired()! gameId => {}", gameExpired.getGameId());
		}
		finally {
			if (lock != null) {
				lock.unlock();
				log.debug("onGameExpired: Unlocked game {}!", gameId);
			}
		}
	}
	
	public void onGameOver(GameOver gameOver) {
		log.debug("onGameOver() # gameId => {}, winnerId => {}", gameOver.getGameId(), gameOver.getWinnerId());
		
		/*
		 * Critical Area, which must be synchronized, consists of:
		 * 
		 * - updateNewGame()
		 * - deleteNewGame()
		 * - onGameExpired()
		 * - onGameOver()
		 * 
		 */
		
		Lock lock = null;
		long gameId = 0;
		
		try {
			// Lock this game
			lock = Grid.getGameLock( gameOver.getGameId() );
			lock.lock();
			gameId = gameOver.getGameId();
			log.debug("onGameOver: Locked game {}!", gameId);
			
			User winner = null;
			
			if (gameOver.getWinnerId() == -1) {
				winner = dataManager.getUserById(User.ManagerId.GAME_DRAW);
			}
			else {
				winner = dataManager.getUserById(gameOver.getWinnerId());
			}
			
			Game game = dataManager.getGameById(gameOver.getGameId());
			game.setState(State.GAME_OVER);
			game.setWinnerId(winner.getId());
			dataManager.updateGame(game.getId(), game);
			
			// Delete corresponding NewGame object in cluster
			cleanUpNewGame(gameOver.getGameOtp());
		}
		catch (TCGException ignored) {
			log.error("TCGException during onGameOver()! gameId => {}", gameOver.getGameId());
		}
		finally {
			if (lock != null) {
				lock.unlock();
				log.debug("onGameOver: Unlocked game {}!", gameId);
			}
		}
	}
	
	/* ######################################################################## */
	/* ######################################################################## */
	/* ######################################################################## */
	
	private void cleanUp(String otp, long gameId) throws TCGException {
		cleanUpNewGame(otp);
		cleanUpGame(gameId);
	}
	
	private void cleanUpNewGame(String otp) {
		dataManager.deleteNewGame(otp);
	}
	
	private void cleanUpGame(long gameId) throws TCGException {
		dataManager.deleteGame(gameId);
	}

}
